Simple encoding
This small library has no dependencies and adds interfaces and classes that you can use to encode and decode your own classes to a JSON compatible structure. This enables you to build powerful API's and storage that can automigrate to newer versions on the fly.
Encoding and decoding can get customized, and that's the whole point of this library. You'll need to write some minor boilerplate code, but that will allow you to customize and work fast. You can also use the decorators and write almost no boilerplate code.
Installation
Yarn
yarn add @simonbackx/simple-encoding
NPM
npm install @simonbackx/simple-encoding
Features
- Great errors out of the box
- Customize as you wish:
- By not using automatic generation of the encoded format, you can easily let the field names and types differ between the encoded version and your actual instances.
- You can add your own decoders/validators for custom fields (e.g. phone number)
- Minor and readable boilerplate code
Todo
Usage example
Decorators
The shortest version you can have, with experimental TypeScript decorators.
import { Encodeable, StringDecoder, NumberDecoder, field } from "@simonbackx/simple-encoding";
class MyClass extends AutoEncoder {
@field({ decoder: NumberDecoder })
id: number;
@field({ decoder: StringDecoder })
name: string;
@field({ decoder: MyClass, optional: true })
other?: MyClass;
writeName() {
console.log(this.name);
}
}
const test = MyClass.create({ id: 123, name: "Hello world" });
const test2 = MyClass.create({ id: 123, name: "Hello world", other: test });
const json = JSON.stringify(test2.encode({ version: 1 }));
const plainObject = JSON.parse(json);
const data = new ObjectData(plainObject, { version: 1 });
const instance = MyClass.decode(data);
instance.writeName();
Now, say we want to change the type of id to a string and we want to have an extra field called createdAt. We can use versioning for this:
class MyClass extends AutoEncoder {
@field({ decoder: NumberDecoder })
@field({ decoder: StringDecoder, version: 2, upgrade: (old: number) => old.toString(), downgrade: (n: string) => parseInt(n) })
id: number;
@field({ decoder: StringDecoder })
name: string;
@field({ decoder: MyClass, optional: true })
other?: MyClass;
@field({ decoder: DateDecoder, version: 2, upgrade: () => new Date() })
createdAt: Date;
writeName() {
console.log(this.name);
}
}
You must update your 'create' methods everywhere, but the encoding and decoding in the older versions keep working:
const myClass = MyClass.create({ id: 123, name: "Hello world" });
const json = JSON.stringify(myClass.encode({ version: 1 }));
const plainObject = JSON.parse(json);
const data = new ObjectData(plainObject, { version: 1 });
const instance = MyClass.decode(data);
The versions in encode an decode should always match. You'll need to store the version depending on the usage. E.g. in an API you'll need to send the version in the URL or in a separate header. In your database you can add an extra field 'version' to save the latest version you used for encoding, same for localstorage, disk storage...
The versioning system allows your API to update the backend before updating the frontend, because the newer version will always be able to communicate with older versions.
const myClass = MyClass.create({ id: "123", name: "This example keeps all data", createdAt: new Date() });
const json = JSON.stringify(myClass.encode({ version: 2 }));
const plainObject = JSON.parse(json);
const data = new ObjectData(plainObject, { version: 2 });
const instance = MyClass.decode(data);
Vanilla (without decorators)
This version makes it possible to do more customization, at the cost of more boilerplate code. This is very usefull when building custom encoders and decoders that have complex behaviour.
import { Encodeable, Data, ObjectData } from "@simonbackx/simple-encoding";
class MyClass implements Encodeable {
id: number;
name: string;
other?: MyClass;
constructor(data: { id: number; name: string; other?: MyClass }) {
this.id = data.id;
this.name = data.name;
this.other = data?.other;
}
writeName() {
console.log(this.name);
}
static decode(data: Data): MyClass {
return new MyClass({
id: data.field("id").number,
name: data.field("name").string,
other: data.optionalField("other")?.decode(MyClass),
});
}
encode(context) {
return {
id: this.id,
name: this.name,
other: this.other?.encode(context),
};
}
}
const test = new MyClass({ id: 123, name: "Hello world" });
const test2 = new MyClass({ id: 123, name: "Hello world", other: test });
const json = JSON.stringify(test2.encode({ version: 1 }));
const plainObject = JSON.parse(json);
const data = new ObjectData(plainObject, { version: 1 });
const instance = MyClass.decode(data);
instance.writeName();